<?php

namespace Dls\Entity\V0;

use Closure;
use Dls\Entity\V0\Exception\Handler;
use Dls\Entity\V0\Grid\Column;
use Dls\Entity\V0\Grid\Displayers\Actions;
use Dls\Entity\V0\Grid\Exporters\AbstractExporter;
use Dls\Entity\V0\Grid\HeaderTools;
use Dls\Entity\V0\Grid\Model;
use Dls\Entity\V0\Grid\Output;
use Dls\Entity\V0\Grid\Row;
use Dls\Entity\V0\Grid\Exporter;
use Dls\Entity\V0\Grid\Tools;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;

class Grid
{
    /**
     * The grid data model instance.
     *
     * @var Model
     */
    protected $model;

    /**
     * Collection of all grid columns.
     *
     * @var \Illuminate\Support\Collection
     */
    protected $columns;

    /**
     * Collection of table columns.
     *
     * @var \Illuminate\Support\Collection
     */
    protected $dbColumns;

    /**
     * row object
     *
     * @var Row
     */
    protected $row;

    /**
     * Rows callable fucntion.
     *
     * @var \Closure
     */
    protected $rowsCallback;

    /**
     * All column names of the grid.
     *
     * @var array
     */
    public $columnNames = [];

    /**
     * Grid builder.
     *
     * @var \Closure
     */
    protected $builder;

    /**
     * Mark if the grid is builded.
     *
     * @var bool
     */
    protected $builded = false;

    /**
     * All variables in grid view.
     *
     * @var array
     */
    protected $variables = [];

    /**
     * Resource path of the grid.
     *
     * @var
     */
    protected $resourcePath;

    /**
     * Default primary key name.
     *
     * @var string
     */
    protected $keyName = '';

    /**
     * View for grid to render.
     *
     * @var string
     */
    protected $view = 'entity::grid.grid';

    /**
     * Per-page options.
     *
     * @var array
     */
    public $perPages = [10, 20, 30, 50, 100];

    /**
     * Default items count per-page.
     *
     * @var int
     */
    public $perPage = 20;

    /**
     * Header tools.
     *
     * @var Tools
     */
    public $tools;

    /**
     * Callback for grid actions.
     *
     * @var Closure
     */
    protected $actionsCallback;

    /**
     * Export driver
     *
     * @var string
     */
    protected $exporter;

    /**
     * Options for grid.
     *
     * @var array
     */
    protected $options = [
        'usePagination'  => true,
        'useActions'     => true,
        'allowCreate'    => true,
        'allowEdit'      => true,
        'allowExporter'    => true,
    ];

    /**
     * @var array
     */
    protected $defaultClass = ['table', 'table-striped', ' table-hover', 'text-center', 'table-bordered'];

    /**
     * 回调用于table的id属性，用于更新表格
     *
     * @var string
     */
    protected $callbackId = 'table-list';

    /**
     * 表格组件的请求地址
     *
     * @var string
     */
    protected $tableUrl = '';

    /**
     * 启用分页器
     *
     * @var int
     */
    protected $pageNum = 20;

    /**
     * 设置data-fn
     *
     * @var array
     */
    protected $dataFn = ['data-table'];

    /**
     * 头部的工具栏
     *
     * @var HeaderTools
     */
    public $headerTools = [];

    /**
     * Output driver
     *
     * @var string
     */
    private $output;

    /**
     * 设置查询条件
     *
     * @var
     */
    private $queryConditions = [];


    /**
     * Create a new grid instance.
     *
     * @param Eloquent $model
     * @param Closure  $builder
     */
    public function __construct(Eloquent $model, Closure $builder)
    {
        $this->keyName = $model->getKeyName();
        $this->model = new Model($model);
        $this->columns = new Collection();
        $this->rows = new Row();
        $this->builder = $builder;

        $this->setupTools();
        $this->setHeaderTools();

        $this->handleExportRequest();
        $this->handleAjaxData();
    }

    /**
     * Setup grid tools.
     */
    public function setupTools()
    {
        $this->tools = new Tools($this);
    }

    /**
     * Setup grid header tools
     */
    public function setHeaderTools()
    {
        $this->headerTools = new HeaderTools($this);
    }

    /**
     * Get or set option for grid.
     *
     * @param string $key
     * @param mixed  $value
     *
     * @return $this|mixed
     */
    public function option($key, $value = null)
    {
        if (is_null($value)) {
            return $this->options[$key];
        }

        $this->options[$key] = $value;

        return $this;
    }

    /**
     * Get primary key name of model.
     *
     * @return string
     */
    public function getKeyName()
    {
        return $this->keyName ?: 'id';
    }

    /**
     * Add column to Grid.
     *
     * @param string $name
     * @param string $label
     *
     * @return Column
     */
    public function column($name, $label = '')
    {
        $relationName = $relationColumn = '';

        if (strpos($name, '.') !== false) {
            list($relationName, $relationColumn) = explode('.', $name);

            $relation = $this->model()->eloquent()->$relationName();

            $label = empty($label) ? ucfirst($relationColumn) : $label;

            $name = snake_case($relationName).'.'.$relationColumn;
        }

        $column = $this->addColumn($name, $label);

        if (isset($relation) && $relation instanceof Relation) {
            $this->model()->with($relationName);
            $column->setRelation($relationName, $relationColumn);
        }

        return $column;
    }

    /**
     * Batch add column to grid.
     *
     * @example
     * 1.$grid->columns(['name' => 'Name', 'email' => 'Email' ...]);
     * 2.$grid->columns('name', 'email' ...)
     *
     * @param array $columns
     *
     * @return Collection|null
     */
    public function columns($columns = [])
    {
        if (func_num_args() == 0) {
            return $this->columns;
        }

        if (func_num_args() == 1 && is_array($columns)) {
            foreach ($columns as $column => $label) {
                $this->column($column, $label);
            }

            return;
        }

        foreach (func_get_args() as $column) {
            $this->column($column);
        }
    }

    /**
     * Add column to grid.
     *
     * @param string $column
     * @param string $label
     *
     * @return Column
     */
    protected function addColumn($column = '', $label = '')
    {
        $column = new Column($column, $label);
        $column->setGrid($this);

        return $this->columns[] = $column;
    }

    /**
     * Get Grid model.
     *
     * @return Model
     */
    public function model()
    {
        return $this->model;
    }

    /**
     * Paginate the grid.
     *
     * @param int $perPage
     *
     * @return void
     */
    public function paginate()
    {
        $this->perPage = request()->input('pageNum');

        $this->model()->paginate($this->perPage);
    }

    /**
     * Disable grid pagination.
     *
     * @return $this
     */
    public function disablePagination()
    {
        $this->model->usePaginate(false);

        $this->option('usePagination', false);

        return $this;
    }

    /**
     * If this grid use pagination.
     *
     * @return bool
     */
    public function usePagination()
    {
        return $this->option('usePagination');
    }

    /**
     * Set per-page options.
     *
     * @param array $perPages
     */
    public function perPages(array $perPages)
    {
        $this->perPages = $perPages;
    }

    /**
     * Disable all actions.
     *
     * @return $this
     */
    public function disableActions()
    {
        return $this->option('useActions', false);
    }

    /**
     * Set grid action callback.
     *
     * @param Closure $callback
     *
     * @return $this
     */
    public function actions(Closure $callback)
    {
        $this->actionsCallback = $callback;

        return $this;
    }

    /**
     * Add `actions` column for grid.
     *
     * @return void
     */
    protected function appendActionsColumn()
    {

        $grid = $this;
        $callback = $this->actionsCallback;

        if (!$this->option('useActions')) {
            return;
        }
        // 操作
        $this->tools->append((new Actions($grid))->display($callback));

    }

    /**
     * Build the grid.
     *
     * @return $this|;
     */
    public function build()
    {

        call_user_func($this->builder, $this);

        $this->model()->setColumn($this->columns);
        return $this;
    }

    /**
     * get all tools.
     *
     * @param Closure $callback
     *
     * @return Collection
     */
    public function tools()
    {
        $this->appendActionsColumn();

        return $this->tools->all();
    }

    /**
     * Remove create button on grid.
     *
     * @return $this
     */
    public function disableCreateButton()
    {
        return $this->option('allowCreate', false);
    }

    /**
     * Remove export button on grid.
     *
     * @return $this
     */
    public function disableExportButton()
    {
        return $this->option('allowExporter', false);
    }

    /**
     * If allow creation.
     *
     * @return bool
     */
    public function allowCreation()
    {
        return $this->option('allowCreate');
    }

    /**
     * If allow export.
     *
     * @return bool
     */
    public function allowExport()
    {
        return $this->option('allowExporter');
    }

    /**
     * If allow creation.
     *
     * @return bool
     */
    public function allowEdit()
    {
        return $this->option('allowEdit');
    }

    /**
     * Render create button for grid.
     *
     * @return Tools\CreateButton
     */
    public function renderCreateButton()
    {
        return new Tools\CreateButton($this);
    }

    /**
     * Render Table Header Tool
     *
     * @return string
     */
    public function renderHeaderTools()
    {
        $this->headerTools
            ->append(new Tools\CreateButton($this))
            ->append(new Tools\ExportButton($this));

        return $this->headerTools->render();
    }

    /**
     * Get current resource uri.
     *
     * @param string $path
     *
     * @return string
     */
    public function resource($path = null)
    {
        if (!empty($path)) {
            $this->resourcePath = $path;

            return $this;
        }

        if (!empty($this->resourcePath)) {
            return $this->resourcePath;
        }

        return rtrim(app('request')->getPathInfo(), '/').'?'.app('request')->getQueryString();
    }

    /**
     * Get the table columns for grid.
     *
     * @return void
     */
    protected function setDbColumns()
    {
        $connection = $this->model()->eloquent()->getConnectionName();

        $this->dbColumns = collect(Schema::connection($connection)->getColumnListing($this->model()->getTable()));
    }

    /**
     * Handle table column for grid.
     *
     * @param string $method
     * @param string $label
     *
     * @return bool|Column
     */
    protected function handleTableColumn($method, $label)
    {
        if (empty($this->dbColumns)) {
            $this->setDbColumns();
        }

        if ($this->dbColumns->has($method)) {
            return $this->addColumn($method, $label);
        }

        return false;
    }

    /**
     * Handle get mutator column for grid.
     *
     * @param string $method
     * @param string $label
     *
     * @return bool|Column
     */
    protected function handleGetMutatorColumn($method, $label)
    {
        if ($this->model()->eloquent()->hasGetMutator($method)) {
            return $this->addColumn($method, $label);
        }

        return false;
    }

    /**
     * Handle relation column for grid.
     *
     * @param string $method
     * @param string $label
     *
     * @return bool|Column
     */
    protected function handleRelationColumn($method, $label)
    {
        $model = $this->model()->eloquent();

        if (!method_exists($model, $method)) {
            return false;
        }
        if (!($relation = $model->$method()) instanceof Relation) {
            return false;
        }

        if ($relation instanceof HasOne || $relation instanceof BelongsTo) {
            $this->model()->with($method);

            return $this->addColumn($method, $label)->setRelation(snake_case($method));
        }

        if ($relation instanceof HasMany || $relation instanceof BelongsToMany || $relation instanceof MorphToMany) {
            $this->model()->with($method);

            return $this->addColumn(snake_case($method), $label);
        }

        return false;
    }

    /**
     * Dynamically add columns to the grid view.
     *
     * @param $method
     * @param $arguments
     *
     * @return Column
     */
    public function __call($method, $arguments)
    {

        $label = isset($arguments[0]) ? $arguments[0] : ucfirst($method);
        if ($column = $this->handleGetMutatorColumn($method, $label)) {
            return $column;
        }

        if ($column = $this->handleRelationColumn($method, $label)) {

            return $column;
        }

        if ($column = $this->handleTableColumn($method, $label)) {
            return $column;
        }

        return $this->addColumn($method, $label);
    }

    /**
     * Add variables to grid view.
     *
     * @param array $variables
     *
     * @return $this
     */
    public function with($variables = [])
    {
        $this->variables = $variables;

        return $this;
    }

    /**
     * table default class
     *
     * @return string
     */
    public function getClass()
    {
        return implode($this->defaultClass, ' ');
    }

    /**
     * Get all variables will used in grid view.
     *
     * @return array
     */
    protected function variables()
    {
        $this->variables['grid'] = $this;

        return $this->variables;
    }

    /**
     * Set a view to render.
     *
     * @param string $view
     * @param array  $variables
     */
    public function setView($view, $variables = [])
    {
        if (!empty($variables)) {
            $this->with($variables);
        }

        $this->view = $view;
    }

    /**
     * @return string
     */
    public function getCallbackId(): string
    {
        return $this->callbackId;
    }

    /**
     * 支持top|left|right
     *
     * @param string $callbackId
     */
    public function setCallbackId(string $callbackId)
    {
        $this->callbackId = $callbackId;
        return $this;
    }

    /**
     * 获取表格的请求地址
     * 支持自定义设置 tableUrl 地址 跟 使用默认地址，但设置查询条件
     *
     * @return string
     */
    public function getTableUrl(): string
    {
        return $this->tableUrl ? $this->tableUrl : $this->resource().'?'.Output::formatQuery($this->queryConditions);
    }

    /**
     * 设置表格的请求的地址
     *
     * @param string $tableUrl
     */
    public function setTableUrl(string $tableUrl = '')
    {
        $this->tableUrl = $tableUrl;
        return $this;
    }

    /**
     * 获取分页的条目数
     *
     * @return string
     */
    public function getPageNum(): string
    {
        return $this->pageNum;
    }

    /**
     * 设置data-fn
     *
     * @param string $dataFn
     * @return $this
     */
    public function setDataFn(string $dataFn)
    {
        array_push($this->dataFn, $dataFn);
        return $this;
    }

    /**
     * 获取data-fn
     *
     * @return string
     */
    public function getDataFn()
    {
        return implode(' ', $this->dataFn);
    }

    /**
     * Handle export request.
     *
     * @param bool $forceExport
     */
    protected function handleExportRequest($forceExport = false)
    {

        if (!$scope = request(Exporter::$queryName)) {
            return;
        }

        // clear output buffer.
        if (ob_get_length()) {
            ob_end_clean();
        }
        $this->model()->usePaginate(false);
        if ($this->builder) {
            call_user_func($this->builder, $this);
            $this->getExporter()->export();
        }
        if ($forceExport) {
            $this->getExporter()->export();
        }
    }

    /**
     * get export handle
     *
     * @return AbstractExporter
     */
    protected function getExporter()
    {
        return (new Exporter($this))->resolve($this->exporter);
    }

    /**
     * get output handle
     *
     * @return Grid\Outputs\AbstractOutput
     */
    protected function getOutput()
    {
        return (new Output($this))->resolve($this->output);
    }

    /**
     * Get the export url.
     *
     * @return string
     */
    public function getExportUrl()
    {
        if ($url = $this->getTableUrl()) {
            return $this->resource() . '?' . Exporter::formatExportQuery($url);
        }

        return $this->resource() . '?' . Exporter::formatExportQuery($this->queryConditions);
    }

    /**
     * Set exporter driver for Grid to export.
     *
     * @param $exporter
     *
     * @return $this
     */
    public function exporter($exporter)
    {
        $this->exporter = $exporter;
        return $this;
    }

    /**
     * Set exporter driver for Grid to export.
     *
     * @param $exporter
     *
     * @return $this
     */
    public function output($output)
    {
        $this->output = $output;
        return $this;
    }

    /**
     * Get the string contents of the grid view.
     *
     * @return string
     */
    public function render()
    {
        $this->handleExportRequest(true);

//        try {
        $this->build();
//        } catch (\Exception $e) {
//            return Handler::renderException($e);
//        }

        return view($this->view, $this->variables())->render();
    }

    /**
     * Get the string contents of the grid view.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->render();
    }

    private function handleAjaxData()
    {
        if (!$scope = request(Output::$queryName)) {
            return;
        }

        $this->getOutput()->data();
    }

    /**
     * @return mixed
     */
    public function getQueryConditions()
    {
        return $this->queryConditions;
    }

    /**
     * @param array $queryConditions
     */
    public function setQueryConditions(array $queryConditions)
    {
        $this->queryConditions = $queryConditions;
        return $this;
    }
}
